﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Net;
using System.Net.Sockets;
using System.IO;
using System.Threading;

namespace NetworkLibrary
{
    public static class Common
    {
        private static int bufferLength = 1024;

        #region Delegates
        public delegate void DataReceived(NetworkData networkData);
        public delegate void OnClientConnected(NetworkClient client);
        public delegate void OnFileReceived(FileReceivedData fileData);
        public delegate void OnClientDisConnected(NetworkClient client);
        public delegate void Log(string text);
        public delegate void OnShuttingDown(Socket client);
        public delegate void ProgressUpdate(FileReceivedData fileData);
        public delegate void OnFileTransferStart(FileReceivedData fileData);
        #endregion

        #region SendSmallDataChunks
        private static bool CanSendReceive(Socket client)
        {
            return client.Connected;
        }
        public static bool Send(Socket client, bool value)
        {
            byte[] data = new byte[1];
            data = BitConverter.GetBytes(value);
            int sent = 0;
            if (client.Connected)
                while (sent < 1)
                    sent += client.Send(data, 1, SocketFlags.None);
            return value;
        }
        public static void Send(Socket client, byte[] hash)
        {
            int sent = 0;
            if (client.Connected)
                while (sent < hash.Length)
                    sent += client.Send(hash, hash.Length, SocketFlags.None);
        }
        public static int Send(Socket client, int value)
        {
            byte[] data = new byte[4];
            data = BitConverter.GetBytes(value);
            int sent = 0;
            if (client.Connected)
                while(sent < 4)
                    sent += client.Send(data, 4, SocketFlags.None);
            return value;
        }
        public static long Send(Socket client, long value)
        {
            byte[] data = new byte[8];
            data = BitConverter.GetBytes(value);
            if (client.Connected)
                client.Send(data, 8, SocketFlags.None);
            return value;
        }
        public static string Send(Socket client, string value)
        {
            byte[] data = new byte[value.Length];
            data = Encoding.ASCII.GetBytes(value);
            int sent = 0;
            if (client.Connected)
                while(sent < data.Length)
                    sent += client.Send(data, data.Length, SocketFlags.None);
            return value;
        }
        public static bool ReceiveBool(Socket client)
        {
            byte[] data = new byte[1];
            int received = 0;
            if (client.Connected)
            {
                while(received < 1)
                    received += client.Receive(data, 0, 1, SocketFlags.None);
            }
            return BitConverter.ToBoolean(data, 0);
        }
        public static int ReceiveInt(Socket client)
        {
            byte[] data = new byte[4];
            int received = 0;
            if (client.Connected)
                while(received < 4)
                    received += client.Receive(data, 0, 4, SocketFlags.None);
            return BitConverter.ToInt32(data, 0);
        }
        public static byte[] ReceiveHash(Socket client)
        {
            byte[] data = new byte[16];
            int received = 0;
            if (client.Connected)
                while (received < 16)
                    received += client.Receive(data, 0, 16, SocketFlags.None);
            return data;
        }
        public static long ReceiveLong(Socket client)
        {
            byte[] data = new byte[8];
            int received = 0;
            if (client.Connected)
                while (received < 8)
                    received += client.Receive(data, 0, 8, SocketFlags.None);
            return BitConverter.ToInt64(data, 0);
        }
        public static string ReceiveString(Socket client, int length)
        {
            byte[] data = new byte[length];
            int received = 0;
            if (client.Connected)
                while (received < length)
                    received += client.Receive(data, received, length, SocketFlags.None);
            return Encoding.ASCII.GetString(data, 0, length);
        }
        #endregion

        #region Commands
        public static NetworkData ReceiveNonChunkedData(NetworkClient clientInfo)
        {
            Socket client = clientInfo.ClientSocket;
            if (client.Connected)
            {
                EncryptedTransfer transfer = clientInfo.Encryption;
                bool encrypted = ReceiveBool(client);
                int totalSize = ReceiveInt(client);
                int dataType = ReceiveInt(client);
                int encryptedSize = totalSize;
                if (encrypted)
                {
                    transfer.prepDecrypt();
                    encryptedSize = ReceiveInt(client);
                }
                byte[] data = new byte[encryptedSize];
                int toReceive = encryptedSize;
                int received = 0;
                while (received < toReceive)
                {
                    try
                    {
                        received += client.Receive(data, received, toReceive - received, SocketFlags.None);
                    }
                    catch (SocketException ex)
                    {
                        if (ex.SocketErrorCode == SocketError.WouldBlock ||
                            ex.SocketErrorCode == SocketError.IOPending ||
                            ex.SocketErrorCode == SocketError.NoBufferSpaceAvailable)
                        {
                            // socket buffer is probably empty, wait and try again
                            Thread.Sleep(30);
                        }
                        else
                            throw ex;  // any serious error occurr
                    }
                }
                if (encrypted)
                    data = transfer.Decrypt(data);
                return new NetworkData(data, dataType, clientInfo.Encrypting, 1, clientInfo);
            }
            return new NetworkData(new byte[0], -999, false, 0, clientInfo);
        }
        public static bool SendNonChunkedData(byte[] data, int dataType, NetworkClient clientInfo)
        {
            Socket client = clientInfo.ClientSocket;
            if (client.Connected)
            {
                EncryptedTransfer transfer = clientInfo.Encryption;

                //Send Whether Encrypted
                Send(client, clientInfo.Encrypting);
                //Send Data Size
                int totalSize = Send(client, data.Length);
                //Send Data Type
                Send(client, dataType);

                if (clientInfo.Encrypting)
                {
                    //Encrypt Data
                    transfer.prepEncrypt();
                    data = transfer.Encrypt(data);

                    //Send Encrypted Data Length
                    Send(client, data.Length);
                }
                client.Send(data, 0, data.Length, SocketFlags.None);
                return true;
            }
            return false;
        }
        #region ReceiveData
        /// <summary>
        /// Receives custom Data of your choice
        /// </summary>
        /// <param name="clientInfo">Information of the client that's ending the data</param>
        /// <returns></returns>
        public static NetworkData ReceiveData(NetworkClient clientInfo)
        {
            Socket client = clientInfo.ClientSocket;
            if (client.Connected)
            {
                //Used if the received data is encrypted
                EncryptedTransfer transfer = clientInfo.Encryption;

                bool encryptedData = ReceiveBool(client);
                int totalSize = ReceiveInt(client);
                int dataType = ReceiveInt(client);

                //Prepares the encrypter to Decrypt
                if (encryptedData)
                    transfer.prepDecrypt();

                byte[] data = new byte[totalSize];

                long sizeReceived = 0;
                int pos = 0;
                int chunksReceived = 0;

                byte[] tdata;

                //While loop that gets all of the data from the server and puts it into one large byte array
                while (sizeReceived < totalSize)
                {
                    //Buffer size will always be the BufferLength unless it's encrypted
                    int bufferSize = BufferLength;

                    //Should always receive BufferLength in size.
                    sizeReceived += bufferSize;

                    //If it's being encrypted then the BufferLength could be a lot longer
                    if (encryptedData)
                        bufferSize = ReceiveInt(client);

                    //A temporary byte array for storing received data, and then pushing into the data array
                    tdata = new byte[bufferSize];

                    int toReceive = ReceiveInt(client);
                    int received = 0;
                    while (received < toReceive)
                    {
                        try
                        {
                            received += client.Receive(tdata, received, toReceive - received, SocketFlags.None);
                        }
                        catch (SocketException ex)
                        {
                            if (ex.SocketErrorCode == SocketError.WouldBlock ||
                                ex.SocketErrorCode == SocketError.IOPending ||
                                ex.SocketErrorCode == SocketError.NoBufferSpaceAvailable)
                            {
                                // socket buffer is probably empty, wait and try again
                                Thread.Sleep(30);
                            }
                            else
                                throw ex;  // any serious error occurr
                        }
                    }


                    //Decrypts the data if needed
                    if (encryptedData)
                        tdata = transfer.Decrypt(tdata);


                    //Copy the data over to the data array
                    for (int i = 0; i < BufferLength && pos < data.Length; i++, pos++)
                        data[pos] = tdata[i];
                    chunksReceived++;
                }
                return new NetworkData(data, dataType, encryptedData, chunksReceived, clientInfo);
            }
            return new NetworkData(new byte[0], -999, false, 0, clientInfo);
        }
        #endregion
        #region ReceiveFile
        /// <summary>
        /// Receives custom Data of your choice
        /// </summary>
        /// <param name="clientInfo">Information of the client that's ending the data</param>
        /// <returns></returns>
        public static FileReceivedData ReceiveFile(NetworkClient clientInfo, string dir, ProgressUpdate progressUpdate, OnFileTransferStart start)
        {
            Socket client = clientInfo.ClientSocket;
            if (client.Connected)
            {
                FileStream stream;
                if (ReceiveBool(client))
                {

                //Used if the received data is encrypted
                EncryptedTransfer transfer = clientInfo.Encryption;

                bool encryptedData = ReceiveBool(client);
                long totalSize = ReceiveLong(client);

                int fileNameLength = ReceiveInt(client);
                string FileName = ReceiveString(client, fileNameLength);

                try
                {
                    stream = File.Create(dir + "\\" + FileName);
                }
                catch (Exception err)
                {
                    //Failed to open the stream
                    //Tell the client not to continue
                    Send(client, false);
                    return new FileReceivedData(FileName, dir, clientInfo, 0, err.Message);
                }
                //We opened our stream, tell client to continue
                Send(client, true);


                if (fileNameLength <= 0)
                    return new FileReceivedData("", "", clientInfo, 0);

                //If the client was able to open their stream


                    //Prepares the encrypter to Decrypt
                    if (encryptedData)
                        transfer.prepDecrypt();

                    long sizeReceived = 0;
                    int pos = 0;
                    int chunksReceived = 0;

                    byte[] tdata;

                    int curProg = 0;
                    FileReceivedData dataInfo = new FileReceivedData(FileName, dir, clientInfo, 0);
                    if (start != null)
                        start(dataInfo);
                    //While loop that gets all of the data from the server and puts it into one large byte array
                    while (sizeReceived < totalSize)
                    {
                        bool temp = false;
                        if (sizeReceived + BufferLength >= totalSize)
                            temp = true;
                        //Buffer size will always be the BufferLength unless it's encrypted
                        int bufferSize = BufferLength;

                        //If it's being encrypted then the BufferLength could be a lot longer
                        if (encryptedData)
                            bufferSize = ReceiveInt(client);

                        //A temporary byte array for storing received data, and then pushing into the data array
                        tdata = new byte[bufferSize];

                        //Receive Buffer Size
                        int toReceive = ReceiveInt(client);
                        int received = 0;
                        while (received < toReceive)
                        {
                            try
                            {
                                received += client.Receive(tdata, received, toReceive - received, SocketFlags.None);
                            }
                            catch (SocketException ex)
                            {
                                if (ex.SocketErrorCode == SocketError.WouldBlock ||
                                    ex.SocketErrorCode == SocketError.IOPending ||
                                    ex.SocketErrorCode == SocketError.NoBufferSpaceAvailable)
                                {
                                    // socket buffer is probably empty, wait and try again
                                    Thread.Sleep(30);
                                }
                                else
                                    throw ex;  // any serious error occurr
                            }
                        }
                        sizeReceived += received;


                        //Decrypts the data if needed
                        if (encryptedData)
                            tdata = transfer.Decrypt(tdata);


                        //Write the data to the file
                        stream.Write(tdata, 0, tdata.Length);
                        chunksReceived++;
                        double val1 = (double)(sizeReceived / 1000000);
                        double val2 = (double)(totalSize / 1000000);
                        double div = val1 / val2;
                        int prg = (int)(div * 100);
                        if (prg > curProg)
                        {
                            dataInfo.SetPercentage(prg);
                            if (progressUpdate != null)
                                progressUpdate(dataInfo);
                            curProg = prg;
                        }
                    }
                    stream.Close();
                    dataInfo.SetPercentage(100);
                    if (progressUpdate != null)
                    progressUpdate(dataInfo);
                    return dataInfo;
                }
            }
            return new FileReceivedData("", "", clientInfo, 0, "Client is possibly not connected");
        }
        #endregion
        #region SendData
        /// <summary>
        /// Sends custom data to a specified client.
        /// </summary>
        /// <param name="data">Byte Array of data.</param>
        /// <param name="dataType">Custom Data Type of an Integer Value. Use an enumeration and cast from ints.</param>
        /// <param name="clientInfo">The Client you're sending to.</param>
        /// <returns></returns>
        public static bool SendData(byte[] data, int dataType, NetworkClient clientInfo)
        {

            Socket client = clientInfo.ClientSocket;
            if (client.Connected)
            {
                client.NoDelay = true;
                client.LingerState = new LingerOption(true, 10);
                if (CanSendReceive(client))
                {
                    EncryptedTransfer transfer = clientInfo.Encryption;

                    //Send Whether Encrypted
                    Send(client, clientInfo.Encrypting);
                    //Send Data Size
                    int totalSize = Send(client, data.Length);
                    //Send Data Type
                    Send(client, dataType);

                    //If Encrypting, prepare the encrypter to start encrypting
                    if (clientInfo.Encrypting)
                        transfer.prepEncrypt();

                    int sent = 0;
                    while (sent < totalSize)
                    {
                        //Defaults to the BufferLength
                        byte[] tdata = new byte[BufferLength];

                        //Keep track of i, data array section may have less than BufferLength available
                        int i = BufferLength;
                        for (int j = 0; j < BufferLength; j++, sent++)
                        {
                            if (sent < totalSize)
                                tdata[j] = data[sent];
                            else
                            {
                                i = j; //Ran out of data, make sure we send the appropriate size
                                break;
                            }
                        }
                        //If we're encrypting, Encrypt it, and send the new BufferLength
                        if (clientInfo.Encrypting)
                        {
                            tdata = transfer.Encrypt(tdata);
                            i = tdata.Length;
                        }
                        Send(client, i);
                        //Send the data to the client
                        try
                        {
                            client.Send(tdata, 0, i, SocketFlags.None);
                        }
                        catch (SocketException ex)
                        {
                            if (ex.SocketErrorCode == SocketError.WouldBlock ||
                                ex.SocketErrorCode == SocketError.IOPending ||
                                ex.SocketErrorCode == SocketError.NoBufferSpaceAvailable)
                            {
                                // socket buffer is probably full, wait and try again
                                Thread.Sleep(30);
                            }
                            else
                                throw ex;  // any serious error occurr
                        }
                    }

                    return true;
                }
            }
            return false;
        }
        #endregion

        #region SendFile
        /// <summary>
        /// Sends custom data to a specified client.
        /// </summary>
        /// <param name="data">Byte Array of data.</param>
        /// <param name="dataType">Custom Data Type of an Integer Value. Use an enumeration and cast from ints.</param>
        /// <param name="clientInfo">The Client you're sending to.</param>
        /// <returns></returns>
        public static FileReceivedData SendFile(string file, NetworkClient clientInfo, ProgressUpdate progressUpdate, OnFileTransferStart start)
        {
            string FILENAME = Path.GetFileName(file);
            Socket client = clientInfo.ClientSocket;
            if (client.Connected)
            {
                if (CanSendReceive(client))
                {
                    EncryptedTransfer transfer = clientInfo.Encryption;

                    Stream stream;
                    try
                    {
                        stream = File.Open(file, FileMode.Open);
                    }
                    catch (Exception err)
                    {
                        //Failed to open our stream
                        //Tell the client
                        Send(client, false);
                        return new FileReceivedData(file, "", clientInfo, 0, err.Message, true);
                    }
                    //Send True, We were able to open our stream
                    Send(client, true);
                    //Make sure the client is ready to continue

                        //Send Whether Encrypted
                        Send(client, clientInfo.Encrypting);
                        //Send Data Size
                        long totalSize = Send(client, stream.Length);
                        //Send File Name Length
                        int fileLength = FILENAME.Length;
                        Send(client, fileLength);
                        //Send File Name
                        Send(client, FILENAME);
                        bool continueStreamIsSet = false;
                        continueStreamIsSet = ReceiveBool(client);
                        if (continueStreamIsSet)   // Stopping here, on chance
                        {

                        //If Encrypting, prepare the encrypter to start encrypting
                        if (clientInfo.Encrypting)
                            transfer.prepEncrypt();

                        long sent = 0;
                        string FileName = Path.GetFileName(file);
                        string dir = file.Substring(0, file.IndexOf(FileName));
                        int curProg = 0;
                        FileReceivedData dataInfo = new FileReceivedData(FileName, dir, clientInfo, 0, true);
                        if (start != null)
                            start(dataInfo);
                        while (sent < totalSize)
                        {
                            bool temp = false;
                            if (sent + BufferLength >= totalSize)
                                temp = true;
                            //Defaults to the BufferLength
                            byte[] tdata = new byte[BufferLength];

                            //Keep track of i, data array section may have less than BufferLength available
                            int i = BufferLength;

                            stream.Read(tdata, 0, BufferLength);
                            sent += tdata.Length;
                            i = tdata.Length;

                            //If we're encrypting, Encrypt it, and send the new BufferLength
                            if (clientInfo.Encrypting)
                            {
                                tdata = transfer.Encrypt(tdata);
                                Send(client, tdata.Length);
                                i = tdata.Length;
                            }
                            //Send Buffer Size
                            Send(client, i);
                            //Send the data to the client
                            try
                            {
                                int SentThisRound = 0;
                                while(SentThisRound < i)
                                    SentThisRound += client.Send(tdata, SentThisRound, i - SentThisRound, SocketFlags.None);
                            }
                            catch (SocketException ex)
                            {
                                if (ex.SocketErrorCode == SocketError.WouldBlock ||
                                    ex.SocketErrorCode == SocketError.IOPending ||
                                    ex.SocketErrorCode == SocketError.NoBufferSpaceAvailable)
                                {
                                    // socket buffer is probably full, wait and try again
                                    Thread.Sleep(30);
                                }
                                else
                                {
                                    stream.Close();
                                    throw ex;  // any serious error occurr
                                }
                            }
                            double val1 = (double)(sent / 1000000);
                            double val2 = (double)(totalSize / 1000000);
                            double div = val1 / val2;
                            int prg = (int)(div * 100);
                            if (prg > curProg)
                            {
                                dataInfo.SetPercentage(prg);
                                if (progressUpdate != null)
                                    progressUpdate(dataInfo);
                                curProg = prg;
                            }
                        }
                        dataInfo.SetPercentage(100);
                        if (progressUpdate != null)
                            progressUpdate(dataInfo);
                        stream.Close();
                        return dataInfo;
                    }
                }
            }
            return new FileReceivedData(file, "", clientInfo, 0, "Client may not be connected", true); ;
            
        }
        #endregion

        #endregion

        public static int BufferLength { get { return bufferLength; } }

        public static Client Client
        {
            get
            {
                throw new System.NotImplementedException();
            }
            set
            {
            }
        }

        public static Server Server
        {
            get
            {
                throw new System.NotImplementedException();
            }
            set
            {
            }
        }
    }

    #region Structs
    /// <summary>
    /// Contains all information for a Server-Client Connection
    /// Socket
    /// Client Name (Nickname)
    /// Encryption
    /// Encrypted Bool (Whether it's currently encrypting its data)
    /// </summary>
    public struct NetworkClient
    {
        Socket client;
        string clientName;
        EncryptedTransfer encryption;
        bool encrypting;
        RSAParameters publicKey;
        
        public NetworkClient(Socket client, string clientName, RSAParameters publicKey)
        {
            encrypting = false;
            encryption = null;
            this.client = client;
            this.clientName = clientName;
            this.publicKey = publicKey;
        }
        #region Accessors
        public void SetEncyrption(EncryptedTransfer transfer)
        {
            encryption = transfer;
            encrypting = true;
        }
        public RSAParameters PublicKey { get { return publicKey; }}
        public Socket ClientSocket { get { return client; } set { client = value; } }
        public string ClientName { get { return clientName; } set { clientName = value; } }
        public EncryptedTransfer Encryption { get { return encryption; } set { encryption = value; encrypting = true; } }
        public bool Encrypting { get { return encrypting; } set { encrypting = value; } }
        #endregion
    }
    /// <summary>
    /// Stores all information on a Sent Network Data
    /// </summary>
    public struct NetworkData
    {
        byte[] data;
        int dataType, chunksReceived;
        bool receivedEncrypted;
        NetworkClient clientInfo;
        public NetworkData(byte[] data, int dataType, bool receivedEncrypted, int chunksReceived, NetworkClient clientInfo)
        {
            this.data = data;
            this.dataType = dataType;
            this.receivedEncrypted = receivedEncrypted;
            this.chunksReceived = chunksReceived;
            this.clientInfo = clientInfo;
        }
        #region Accessors
        public byte[] Data { get { return data; } }
        public int DataType { get { return dataType; } }
        public bool ReceivedEncrypted { get { return receivedEncrypted; } }
        public int ChunksReceived { get { return chunksReceived; } }
        public NetworkClient ClientInfo { get { return clientInfo; } }
        #endregion
    }
    /// <summary>
    /// Stores file received information
    /// </summary>
    public struct FileReceivedData
    {
        string fileName;
        string dir;
        int percentage;
        NetworkClient clientInfo;
        string errorCode;
        bool sending;
        public FileReceivedData(string fileName, string dir, NetworkClient clientInfo, int percentage)
        {
            this.fileName = fileName;
            this.dir = dir;
            this.clientInfo = clientInfo;
            this.percentage = percentage;
            sending = false;
            errorCode = "";
        }
        public FileReceivedData(string fileName, string dir, NetworkClient clientInfo, int percentage, bool sending)
        {
            this.sending = sending;
            this.fileName = fileName;
            this.dir = dir;
            this.clientInfo = clientInfo;
            this.percentage = percentage;
            errorCode = "";
        }
        public FileReceivedData(string fileName, string dir, NetworkClient clientInfo, int percentage, string error)
        {
            this.fileName = fileName;
            this.dir = dir;
            this.clientInfo = clientInfo;
            this.percentage = percentage;
            this.errorCode = error;
            sending = false;
        }
        public FileReceivedData(string fileName, string dir, NetworkClient clientInfo, int percentage, string error, bool sending)
        {
            this.sending = sending;
            this.fileName = fileName;
            this.dir = dir;
            this.clientInfo = clientInfo;
            this.percentage = percentage;
            this.errorCode = error;
        }
        #region Accessors
        public void SetPercentage(int value)
        {
            percentage = value;
        }
        public string Error { get { return errorCode; } }
        public string FileName { get { return fileName; } }
        public string Directory { get { return dir; } }
        public NetworkClient ClientInfo { get { return clientInfo; } }
        public int Percentage { get { return percentage; } set { percentage = value; } }
        public bool Sending { get { return sending; } }
        #endregion
    }
    #endregion
}
